﻿namespace MapleRock.P2PEmulator.Core
{
  using System;
  using System.Collections.Generic;
  using System.Collections.ObjectModel;
  using System.Diagnostics;
  using System.Linq;
  using System.Net;
  using System.Text;

  public enum PeerState
  {
    Initial = 0,
    Bootstrap = 1,
    Correction = 2,
    MainLoop = 3,
    Error = 4,
  }

  public enum PeerBehaviour
  { 
    SendRandomRequests = 1,
    ProcessRequests = 2,
    DoesNothing = 3,
  }

  public sealed class Peer
  {
    private Random randomizer = null;

    private List<PeerConnectivityInfo> connectionTable = new List<PeerConnectivityInfo>();

    private PeerConnectivityInfo[] nearestPeers = null;

    public Peer(
      uint id, 
      uint cityId, 
      IPAddress address, 
      double[] absPosition, 
      double[] geoPosition, 
      double bandwidthMbs,
      int maximumLinks,
      PeerBehaviour behaviour)
    {
      Debug.Assert(id > 0, "id > 0");
      Debug.Assert(cityId > 0, "cityId > 0");
      Debug.Assert(maximumLinks >= 2, "maximumLinks >= 2");

      this.Incoming = new Queue<Message>();
      this.Outgoing = new Queue<Message>();
      this.Id = id;
      this.CityId = cityId;
      this.Address = address;
      this.AbsolutePosition = absPosition;
      this.GeoPosition = geoPosition;
      this.BandwidthMbs = bandwidthMbs;
      this.randomizer = new Random(Convert.ToInt32(id));
      this.State = PeerState.Initial;

      this.Behaviour = behaviour;

      this.SearchRequestCompletionPeriods = new List<uint>();

      this.MaximumLinks = maximumLinks;
    }

    public delegate void SearchRequestSuccededDelegate(MessageSearchRequest request, uint interval);

    public delegate void SearchRequestFailedDelegate(MessageSearchRequest request, uint interval);

    public event SearchRequestSuccededDelegate OnSearchRequestSucceded;

    public event SearchRequestFailedDelegate OnSearchRequestFailed;

    public double[] AbsolutePosition { get; private set; }

    public IPAddress Address { get; private set; }

    public double BandwidthMbs { get; private set; }

    public PeerBehaviour Behaviour { get; set; }

    public uint CityId { get; private set; }

    public double[] GeoPosition { get; private set; }

    public uint Id { get; private set; }

    public int MaximumLinks { get; set; }

    public Queue<Message> Incoming { get; private set; }

    public Queue<Message> Outgoing { get; private set; }

    public MessageSearchRequest SearchRequest { get; set; }

    public uint SearchRequestTimestamp { get; set; }

    public PeerState State { get; private set; }

    public int SearchRequestsSent { get; private set; }

    public int SearchRequestsSucceded { get; private set; }

    public int SearchRequestsFailed { get; private set; }

    public List<uint> SearchRequestCompletionPeriods { get; private set; }

    public int TrafficCounter { get; private set; }

    public double SearchRequestsIntensity { get; private set; }
   
    public List<PeerConnectivityInfo> ConnectionTable
    {
      get { return this.connectionTable; }
    }

    public void OnIdle(NetworkState state)
    {
      switch (this.State)
      {
        case PeerState.Initial:
          this.State = PeerState.Bootstrap;
          break;

        case PeerState.Bootstrap:
          this.State = PeerState.Correction;
          break;

        case PeerState.Correction:
          this.MakeCorrection(state);
          this.State = PeerState.MainLoop;
          break;

        case PeerState.MainLoop:
          this.OnIdle_MainLoop(state);
          break;

        case PeerState.Error:
          this.OnIdle_Error(state);
          break;

        default:
          throw new InvalidOperationException("Unexpected peer state.");
      }
    }

    public void ProcessOutgoing(
      NetworkState state, 
      int periodMs)
    {
      Debug.Assert(periodMs > 0, "periodMs > 0");
      Debug.Assert(null != state, "null != state");

      if (this.Outgoing.Count == 0)
      {
        return;
      }

      var message = this.Outgoing.Dequeue();
      state.Transport.ScheduleMessage(message);
    }

    public void ResetState()
    {
      this.Incoming.Clear();
      this.Outgoing.Clear();
      this.SearchRequest = null;
      this.SearchRequestCompletionPeriods.Clear();
      this.SearchRequestsFailed = 0;
      this.SearchRequestsIntensity = 0;
      this.SearchRequestsSent = 0;
      this.SearchRequestsSucceded = 0;
      this.SearchRequestTimestamp = 0;
      this.TrafficCounter = 0;
    }

    // ----------------------------------------------------------------
    // private
    // ----------------------------------------------------------------

    private void Behaviour_RandomRequest(NetworkState state)
    {
      Debug.Assert(
        this.Behaviour == PeerBehaviour.SendRandomRequests,
        "this.Behaviour == PeerBehaviour.SendRandomRequests");

      if (this.SearchRequest == null)
      {
        var randomPeer = state.Peers[Convert.ToUInt32(this.randomizer.Next(state.Peers.Count)) + 1];

        this.SearchRequest = new MessageSearchRequest(state.GenerateUniqueId(), this.Address, Guid.NewGuid());
        this.SearchRequest.Destination = (double[])randomPeer.AbsolutePosition.Clone();
        this.SearchRequest.From = this.Id;
        this.SearchRequest.SearchRadius = 1;
        this.SearchRequest.To = this.Id;

        this.SearchRequestTimestamp = state.TimePassedMs;

        this.Outgoing.Enqueue(this.SearchRequest);

        ++this.SearchRequestsSent;

        return;
      }

      if ((state.TimePassedMs - this.SearchRequestTimestamp) > 60000)
      {
        if (this.OnSearchRequestFailed != null)
        {
          this.OnSearchRequestFailed.Invoke(this.SearchRequest, 60000);
        }

        this.SearchRequestsIntensity = 1000.0 / 60000.0;

        this.SearchRequest = null;
      }
    }

    private double EvaluateAggregateAngleWithConnectedPeers(PeerConnectivityInfo info)
    {
      var aggregateAngle = 0.0;

      for (int i = 0; i < this.connectionTable.Count; ++i)
      {
        var connectionInfo = this.connectionTable[i];
        if (connectionInfo == info)
        {
          continue;
        }

        var angle = Geometry.EvaluateAngle(
          this.AbsolutePosition,
          connectionInfo.InfoCoordinates,
          this.AbsolutePosition,
          info.InfoCoordinates);
        Debug.Assert(angle >= 0, "angle >= 0");

        aggregateAngle += angle;
      }

      return aggregateAngle;
    }

    private double[] EvaluateRelativeCoords()
    { 
      var coords = new double[this.AbsolutePosition.Length];

      foreach (var entry in this.connectionTable)
      {
        // skip entries for that i-space coords was not resolved
        if (null == entry.InfoCoordinates)
        {
          continue;
        }

        Debug.Assert(
          entry.InfoCoordinates.Length == coords.Length,
          "entry.InfoCoordinates.Length == coords.Length");

        for (var i = 0; i < coords.Length; ++i)
        {
          coords[i] += entry.InfoCoordinates[i];
        }
      }

      for (var i = 0; i < coords.Length; ++i)
      {
        coords[i] /= Convert.ToDouble(this.connectionTable.Count);
      }

      return coords;
    }

    private int GetIcomingTrafficLimit(int periodMs)
    {
      Debug.Assert(periodMs >= 0, "periodMs >= 0");

      return Convert.ToInt32((this.BandwidthMbs * 1024.0 * 1024.0) * (periodMs / 1000.0));
    }

    private int GetOutgoingTrafficLimit(int periodMs)
    {
      Debug.Assert(periodMs >= 0, "periodMs >= 0");

      return Convert.ToInt32((this.BandwidthMbs * 1024.0 * 1024.0) * (periodMs / 1000.0));
    }

    private void MakeCorrection(NetworkState state)
    {
      state.FreePeers.Remove(this);

      while (this.connectionTable.Count <= this.MaximumLinks)
      {
        var bestNeighbor = this.MakeCorrection_FindBestNeighbor(state);
        if (null == bestNeighbor)
        {
          return;
        }

        Debug.Assert(bestNeighbor.PeerId != this.Id, "bestNeighbor.PeerId != this.Id");

        this.connectionTable.Add(bestNeighbor);

        var bestNeighborPeer = state.Peers[bestNeighbor.PeerId];

        Debug.Assert(
          bestNeighborPeer.ConnectionTable.Count < bestNeighborPeer.MaximumLinks,
          "bestNeighborPeer.ConnectionTable.Count < bestNeighborPeer.MaximumLinks");

        var backLink = new PeerConnectivityInfo();
        backLink.PeerId = this.Id;
        backLink.InfoCoordinates = (double[])this.AbsolutePosition.Clone();
        backLink.SearchRequestsPassed = new List<Guid>();

        bestNeighborPeer.ConnectionTable.Add(backLink);

        state.FreePeers.Remove(bestNeighborPeer);
      }
    }

    private void MakeCorrection_FindNearestPeers(NetworkState state)
    {
      Debug.Assert(null == this.nearestPeers, "null == this.nearestPeers");

      PeersIspDistanceTable.Instance.ResetTable(state.FreePeers, this);

      state.FreePeers.Sort(this.MakeCorrection_FindNearestNeighbor_Comparer);

      var n = this.MaximumLinks;
      n = Math.Min(n, state.FreePeers.Count);

      this.nearestPeers = new PeerConnectivityInfo[n];
      for (var i = 0; i < n; ++i)
      {
        var peer = state.FreePeers[i];
        
        var connectivityInfo = new PeerConnectivityInfo();
        connectivityInfo.PeerId = peer.Id;
        connectivityInfo.InfoCoordinates = peer.AbsolutePosition;
        connectivityInfo.SearchRequestsPassed = new List<Guid>();
        this.nearestPeers[i] = connectivityInfo;
      }
    }

    private int MakeCorrection_FindNearestNeighbor_Comparer(Peer left, Peer right)
    {
      var distanceLeft = PeersIspDistanceTable.Instance.GetDistance(this.Id, left.Id);
      var distanceRight = PeersIspDistanceTable.Instance.GetDistance(this.Id, right.Id);

      if (distanceLeft > distanceRight)
      {
        return 1;
      }

      if (distanceLeft < distanceRight)
      {
        return -1;
      }

      return 0;
    }

    private PeerConnectivityInfo MakeCorrection_FindBestNeighbor(NetworkState state)
    {
      if (null == this.nearestPeers)
      {
        this.MakeCorrection_FindNearestPeers(state);
      }

      // weigh nearest peers and select the best one

      var maxWeight = double.MinValue;
      PeerConnectivityInfo maxWeightLink = null;
      foreach (var connectionInfo in this.nearestPeers)
      {
        bool isAlreadyConnected = this.connectionTable.Any(r => (r.PeerId == connectionInfo.PeerId));
        if (isAlreadyConnected)
        { 
          continue;
        }

        var nearestPeer = state.Peers[connectionInfo.PeerId];

        isAlreadyConnected = nearestPeer.connectionTable.Any(r => (r.PeerId == this.Id));
        if (isAlreadyConnected)
        {
          continue;
        }

        isAlreadyConnected = (nearestPeer.ConnectionTable.Count >= nearestPeer.MaximumLinks);
        if (isAlreadyConnected)
        {
          continue;
        }

        var angle = this.EvaluateAggregateAngleWithConnectedPeers(connectionInfo);

        var distance = Geometry.EvaluateDistance(
          this.AbsolutePosition,
          connectionInfo.InfoCoordinates);

        const double AngleK = 1;
        const double DistanceK = 0;

        var weight = (AngleK * angle) + (DistanceK * distance);

        if (weight > maxWeight)
        {
          maxWeight = weight;
          maxWeightLink = connectionInfo;
        }
      }

      if (maxWeightLink == null)
      {
        return null;
      }

      // return the best peer
      var newEntry = new PeerConnectivityInfo();
      newEntry.PeerId = maxWeightLink.PeerId;
      newEntry.InfoCoordinates = maxWeightLink.InfoCoordinates;

      return newEntry;
    }

    private void OnIdle_Error(NetworkState state)
    {
    }

    private void OnIdle_MainLoop(NetworkState state)
    {
      // process incoming messages

      while (this.Incoming.Count != 0)
      {
        var incomingMessage = this.Incoming.Dequeue();

        this.TrafficCounter += incomingMessage.Size;

        if (incomingMessage is MessageSearchRequest)
        {
          this.ProcessSearchRequest(incomingMessage as MessageSearchRequest, state);
        }

        if (incomingMessage is MessageSearchResponse)
        {
          this.ProcessSearchResponse(incomingMessage as MessageSearchResponse, state);
        }
      }

      if (this.Behaviour == PeerBehaviour.SendRandomRequests)
      {
        this.Behaviour_RandomRequest(state);
      }
    }

    private void ProcessSearchRequest(MessageSearchRequest searchRequest, NetworkState state)
    {
      if (--searchRequest.Ttl <= 0)
      {
        return;
      }

      var delta = Geometry.EvaluateDistance(this.AbsolutePosition, searchRequest.Destination);

      Debug.Assert(searchRequest.SearchRadius > 0, "searchRequest.SearchRadius > 0");
      if (delta < searchRequest.SearchRadius)
      {
        // if current peer is the search request destination
        // send response

        var backId = state.Peers.Values.Single(p => p.Address == searchRequest.BackAddress).Id;

        var response = new MessageSearchResponse(
          state.GenerateUniqueId(), this.Address, searchRequest.RequestId);
        response.From = this.Id;
        response.To = backId;

        this.Outgoing.Enqueue(response);

        return;
      }

      // azimuth starting and ending points 
      // from the current peer position to the destination
      var destinationAzimuth0 = this.AbsolutePosition;
      var destinationAzimuth1 = searchRequest.Destination;

      var routeSelect = new Dictionary<uint, double>();

      foreach (var connection in this.ConnectionTable)
      {
        var peerAzimuth0 = this.AbsolutePosition;
        var peerAzimuth1 = connection.InfoCoordinates;

        var angle = Geometry.EvaluateAngle(
          destinationAzimuth0,
          destinationAzimuth1,
          peerAzimuth0,
          peerAzimuth1);

        if (angle < 0)
        {
          angle += (Math.PI * 2);
        }

        if (angle > Math.PI)
        {
          angle = ((Math.PI * 2) - angle);
        }

        if (connection.SearchRequestsPassed == null)
        {
          connection.SearchRequestsPassed = new List<Guid>();
        }

        var translationCount = connection.SearchRequestsPassed.Count(
          guid => guid == searchRequest.RequestId);

        if (translationCount == 0)
        {
          routeSelect[connection.PeerId] = angle;
        }
      }

      PeerConnectivityInfo mostSuitableConnection = null;
      double bestWeight = double.MaxValue;
      foreach (var entry in routeSelect)
      {
        if (entry.Value < bestWeight)
        {
          bestWeight = entry.Value;
          mostSuitableConnection = this.connectionTable.Single(row => (row.PeerId == entry.Key));
        }
      }

      if (mostSuitableConnection == null)
      {
        var randomIndex = this.randomizer.Next(this.connectionTable.Count);
        mostSuitableConnection = this.connectionTable[randomIndex];
      }
      
      var mostSuitablePeer = state.Peers[mostSuitableConnection.PeerId];

      searchRequest.From = this.Id;
      searchRequest.To = mostSuitablePeer.Id;
      this.Outgoing.Enqueue(searchRequest);

      mostSuitableConnection.SearchRequestsPassed.Add(searchRequest.RequestId);
    }

    private void ProcessSearchResponse(MessageSearchResponse response, NetworkState state)
    {
      Debug.Assert(null != response, "null != response");
      Debug.Assert(
        (PeerBehaviour.SendRandomRequests == this.Behaviour) || (PeerBehaviour.ProcessRequests == this.Behaviour),
        "(PeerBehaviour.SendRandomRequests == this.Behaviour) || (PeerBehaviour.ProcessRequests == this.Behaviour)");

      if (null == this.SearchRequest)
      {
        ++this.SearchRequestsFailed;
        return;
      }

      if (response.RequestId != this.SearchRequest.RequestId)
      {
        ++this.SearchRequestsFailed;
        return;
      }

      this.SearchRequestCompletionPeriods.Add(state.TimePassedMs - this.SearchRequestTimestamp);

      ++this.SearchRequestsSucceded;

      this.SearchRequestsIntensity = 1000.0 / (state.TimePassedMs - this.SearchRequestTimestamp);

      if (this.OnSearchRequestSucceded != null)
      {
        this.OnSearchRequestSucceded.Invoke(
          this.SearchRequest, 
          state.TimePassedMs - this.SearchRequestTimestamp);
      }

      this.SearchRequest = null;
    }
  }
}
